package cn.sciento.export.filler;

import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;

import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.reflect.FieldUtils;
import org.apache.poi.xssf.streaming.SXSSFCell;
import org.apache.poi.xssf.streaming.SXSSFRow;
import org.apache.poi.xssf.streaming.SXSSFSheet;
import org.apache.poi.xssf.streaming.SXSSFWorkbook;
import cn.sciento.core.util.Pair;
import cn.sciento.export.ExcelFiller;
import cn.sciento.export.util.ShardUtils;
import cn.sciento.export.vo.ExportColumn;
import org.springframework.util.Assert;

public class MultiSheetFiller extends ExcelFiller {
    public static final String FILLER_TYPE = "multi-sheet";

    public MultiSheetFiller() {
    }

    public MultiSheetFiller(SXSSFWorkbook workbook) {
        super(workbook);
    }

    public String getFillerType() {
        return "multi-sheet";
    }

    public void fillData(SXSSFWorkbook workbook, ExportColumn exportClass, List<?> data) {
        fillData(exportClass, (String) null, (Object) null, (List<ExportColumn>) null, data);
    }

    private void fillData(ExportColumn exportClass, String parentTitle, Object parentData, List<ExportColumn> parentColumns, List<?> data) {
        String title = (String) Optional.<String>ofNullable(parentTitle).map(t -> t + ".").orElse("") + exportClass.getTitle();
        SXSSFSheet sheet = getFillSheet(title);
        List<ExportColumn> checkedChildren = getCheckedChildren(exportClass);
        List<ExportColumn> innerParentColumns = (List<ExportColumn>) exportClass.getChildren().stream().filter(c -> (c.getExcelColumn().showInChildren() && !c.hasChildren())).collect(Collectors.toList());
        for (int i = 0, len = data.size(); i < len; i++) {
            Assert.isTrue((sheet.getLastRowNum() < 1000000), "export.too-many-data");
            SXSSFRow row = sheet.createRow(sheet.getLastRowNum() + 1);
            fillRow(row, exportClass.getExcelSheet().colOffset(), parentData, parentColumns, data.get(i), exportClass.getChildren());
            for (ExportColumn child : checkedChildren) {
                List<Object> childData = getChildData(data.get(i), child);
                fillDataWithShard(child, title, data.get(i), innerParentColumns, childData);
            }
        }
    }

    private void fillDataWithShard(ExportColumn exportClass, String parentTitle, Object parentData, List<ExportColumn> parentColumns, List<?> data) {
        if (this.singleSheetMaxRow != null) {
            int sum = data.size();
            Pair<Integer, Integer> pair = ShardUtils.shard(sum, this.singleSheetMaxRow.intValue());
            int shardNum = ((Integer) pair.getFirst()).intValue();
            int lastNum = ((Integer) pair.getSecond()).intValue();
            List<List<?>> shardData = ShardUtils.prepareShardData(data, shardNum, this.singleSheetMaxRow.intValue(), lastNum);
            for (int i = 0; i < shardNum; i++)
                fillData(exportClass, parentTitle, parentData, parentColumns, shardData.get(i));
        } else {
            fillData(exportClass, parentTitle, parentData, parentColumns, data);
        }
    }

    protected void createSheetAndTitle(SXSSFWorkbook workbook, ExportColumn root) {
        createSheetAndTitle(workbook, (String) null, (List<ExportColumn>) null, root);
    }

    private void createSheetAndTitle(SXSSFWorkbook workbook, String parentTitle, List<ExportColumn> parentColumns, ExportColumn exportClass) {
        if (exportClass.isChecked() && exportClass.hasChildren()) {
            String title = (String) Optional.<String>ofNullable(parentTitle).map(t -> t + ".").orElse("") + (String) StringUtils.defaultIfBlank(exportClass.getTitle(), exportClass.getName());
            SXSSFSheet sheet = workbook.createSheet(title);
            sheet.setDefaultColumnWidth(20);
            List<ExportColumn> titleColumn = new ArrayList<>();
            if (CollectionUtils.isNotEmpty(parentColumns))
                titleColumn.addAll(parentColumns);
            titleColumn.addAll(exportClass.getChildren());
            createTitleRow(sheet, exportClass.getExcelSheet().rowOffset(), exportClass.getExcelSheet().colOffset(), titleColumn);
            List<ExportColumn> innerParentColumns = (List<ExportColumn>) exportClass.getChildren().stream().filter(c -> (c.getExcelColumn().showInChildren() && !c.hasChildren())).collect(Collectors.toList());
            exportClass.getChildren().forEach(child -> {
                if (child.hasChildren())
                    createSheetAndTitle(workbook, title, innerParentColumns, child);
            });
        }
    }

    private void createTitleRow(SXSSFSheet sheet, int rowOffset, int colOffset, List<ExportColumn> columns) {
        SXSSFRow titleRow = sheet.createRow(rowOffset);
        titleRow.setHeight((short) 350);
        int cells = 0;
        for (ExportColumn column : columns) {
            if (column.isChecked() && !column.getExcelColumn().child()) {
                SXSSFCell cell = titleRow.createCell(colOffset + cells);
                fillCellValue(cell, column.getTitle(), Collections.emptyList(), column.getExcelColumn().pattern(), null);
                cell.setCellStyle(this.titleCellStyle);
                setCellWidth(sheet, column.getType(), colOffset + cells);
                cells++;
            }
        }
    }

    private SXSSFSheet getFillSheet(String title) {
        for (int i = 0; i < this.workbook.getNumberOfSheets(); i++) {
            if (this.workbook.getSheetAt(i).getSheetName().startsWith(title))
                return this.workbook.getSheetAt(i);
        }
        return this.workbook.getSheet(title);
    }

    private void fillRow(SXSSFRow row, int colOffset, Object parentData, List<ExportColumn> parentColumns, Object rowData, List<ExportColumn> columns) {
        int cells = 0;
        if (parentData != null && CollectionUtils.isNotEmpty(parentColumns))
            for (ExportColumn parentColumn : parentColumns) {
                if (!parentColumn.isChecked())
                    continue;
                Object cellValue = null;
                try {
                    cellValue = FieldUtils.readField(parentData, parentColumn.getName(), true);
                } catch (Exception ev) {
                    this.logger.error("get value error.", ev);
                }
                fillCellValue(row.createCell(colOffset + cells++), cellValue, rowData, parentColumn
                                .getExcelColumn().pattern(),
                        Arrays.asList(parentColumn.getExcelColumn().renderers()));
            }
        for (ExportColumn column : columns) {
            if (column.isChecked() && !column.hasChildren()) {
                Object cellValue = null;
                try {
                    cellValue = FieldUtils.readField(rowData, column.getName(), true);
                } catch (Exception ev) {
                    this.logger.error("get value error.", ev);
                }
                fillCellValue(row.createCell(colOffset + cells++), cellValue, rowData, column
                                .getExcelColumn().pattern(),
                        Arrays.asList(column.getExcelColumn().renderers()));
            }
        }
    }

    private List<ExportColumn> getCheckedChildren(ExportColumn root) {
        if (root != null && CollectionUtils.isNotEmpty(root.getChildren()))
            return (List<ExportColumn>) root.getChildren().stream().filter(column -> (column.isChecked() && column.hasChildren())).collect(Collectors.toList());
        return Collections.emptyList();
    }

    private List<Object> getChildData(Object data, ExportColumn child) {
        String getter = "get" + child.getName();
        Method[] methods = data.getClass().getDeclaredMethods();
        for (Method method : methods) {
            if (method.getName().equalsIgnoreCase(getter))
                try {
                    method.setAccessible(true);
                    return ((List) method.invoke(data) == null) ? Collections.<Object>emptyList() : (List<Object>) method.invoke(data);
                } catch (Exception e) {
                    this.logger.error("get child data error.", e);
                }
        }
        return Collections.emptyList();
    }
}
